iT邦幫忙

2022 iThome 鐵人賽

DAY 23
0

今天談談多重繼承的優缺點。


  • 上篇最後的情境-7,以super().show_breed()呼叫父類別的方法,結果每一個類別的方法只會呼叫一次:
    https://ithelp.ithome.com.tw/upload/images/20221007/201484850rkB1kfNbo.png
  • 先跑Timba接著Hardwood很易理解。但Hardwood的上一層是Tree,為甚麼不會先執行,而是讓給ConfiderTree在最後才呼叫呢?
  • 原來Python在多重繼承下,是遵守一套名為Method Resolution Order(MRO)的原則,依照一定的順序搜尋各層別的同名方法,搜尋到就執行,然後終止搜尋。舊版Python搜尋採用DLR or depth-first left to right algorithm(深度優先搜尋演算法或前序遍歷算法),而目前Python的MRO己換用改良後的C3 Linearization Algorithm
  • 該演算法原理解釋起來會占用較多篇幅,直接看code再略加說明就好。
  • 筆者準備挑選一個有六個類別的多重繼承來作例子。選六個類別是類別數量夠多可詳細追蹤MRO,又不至於太過複雜。
  • 之前一直沿用的Tree類別要設計到六層比較麻煩,所以這次決定換個「人設」,以佛教禪宗從初祖達摩(Zen1)到六祖惠能(Zen6),分別代表六個類別,來觀察Python的MRO如何運作:
    class Zen1():  # 初祖
        def show_class_name(self):
            print(__class__.__name__)   # 本class名。
            print(super().__class__.__name__)   # 呼叫父類別(class 'object')名。
    
    class Zen2(Zen1):  # 二祖
        def show_class_name(self)  :
            print(__class__.__name__)
            super().show_class_name()   # 呼叫父類別的show_class_name()。
    
    class Zen3(Zen2, Zen1):  # 三祖
        def show_class_name(self)  :
            print(__class__.__name__)
            super().show_class_name()
    
    class Zen4(Zen3, Zen2, Zen1):  # 四祖
        def show_class_name(self)  :
            print(__class__.__name__)
            super().show_class_name()
    
    class Zen5(Zen2, Zen1):  # 五祖
        def show_class_name(self)  :
            print(__class__.__name__)
            super().show_class_name()
    
    class Zen6(Zen4, Zen5, Zen3):  # 六祖
        def show_class_name(self)  :
            print(__class__.__name__)
            super().show_class_name()
    
    
    zen = Zen6()      # 搜尋程序將從六祖開始。
    zen.show_class_name()
    print()    
    print(Zen6.mro())   # Method Resolution Order
    
  • 輸出:
    https://ithelp.ithome.com.tw/upload/images/20221008/20148485SmQ9coU8As.png
  • 對照以上輸出,C3 Linearization Algorithm的搜尋過程大致如下(註1):
    1. 主程式建立了一個六祖物件,所以先從六祖(Zen6)開始搜尋。;
    2. 步驟1搜尋不到時,找六祖的第一順位父類別四祖(Zen4)。注意:四祖並未被其他類別所繼承,所以是「好」的節點(好的節點才要搜尋);
    3. 四祖的第一順位本來是三祖,但是,不好意思,三祖是六祖的第三順位父類別,而六祖的第二順位父類別五祖還沒搜尋過,所以三祖算是「不好」的節點,不好節點先行跳過;
    4. 回到六祖的第二順位父類別五祖(Zen5)。五祖沒有被其他各祖繼承,是好節點,搜之;
    5. 五祖的第一順位是二祖,但是二祖有被其後的類別(三祖)所繼承。不妙,跳過之;
    6. 再回六祖的第三順位父類別三祖(Zen3)。
    7. 六祖的第一層父類別搜尋完畢。以此類推,搜尋尚未搜尋過的六祖第二層父類別,即四祖和五祖未完成的父類別:二祖(Zen2)和初祖(Zen1)。
    8. C3 Linearization Algorithm的原則是不重複搜索,至此整個搜索程序圓滿結束。
    9. 根據以上描述,搜尋的順序是:Zen6Zen4Zen5Zen3Zen2Zen1
    10. 最後由Zen1呼叫其super(),由於Zen1並無外顯繼承,Python會自動將class object繼承給它。亦即Python萬物皆源出於class object,好比所有人類均來自非洲草原。不過Zen1的print(super().__class__.__name__)印出的是super而不是object就是。

多重繼承的Pros and Cons

Pros

  • 多重繼承允許一個類別有多個父類別,這種設計可以較為精準地模擬現實世界的情況。現實世界的事物關係錯綜複雜、盤根錯節,不可能只有單一繼承模式。
  • 有人認為,子類別擁有多個父類別時,較能重用(reuse)父類別的屬性和方法

Cons

  • 在本篇前半段,筆者花了那麼多時間和精力,反覆解說(雖然也許解說有誤)多重繼承時父子類別搜尋程序(MRO),其實想表達的是:多重繼承雖然強大,卻有雖不致命但算得上嚴重的缺點。上例就很明顯,在繼承體系較深而複雜時,除非對該程式語言所採用的搜尋演算法有一定程度了解,否則使用者有時真的難以了解究竟會呼叫到哪一世代的方法,因而讓多重繼承變得難用、混亂(messy),一不小心就掉入陷阱。
  • 此外,多重繼承在compiler實作時,也比較困難
  • 繼承層級越多,越耗電腦的記憶體,效率可能越低。

多重繼承總結

  • 正因有以上缺點,有人說多重繼承got a poor reputation,名聲不好。
  • 可能由於這個原因,一些較新的物件導向程式語言如Java, C#乾脆不設多重繼承機制。
  • 筆者建議使用多重繼承時,不要太過複雜。父親太多,層別太深不一定是好事。

註1:

  • 此例只是「簡易流程」說明,演算法中有部分地方省略。
  • 感謝Alex & Son Co.公司的PM Eric兄大力支援,協助修正此搜尋流程。

上一篇
John Doe有幾個「老豆」?
下一篇
ABC: 談談Abstract Classes
系列文
Oops! OOPP: An Introduction to Object-Oriented Programming in Python30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言